/*
* Copyright 2013 Philip Schiffer
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file incorporates code covered by the following terms:
* Copyright (c) 2004-2013 QOS.ch
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.psdev.slf4j.android.logger;
import android.util.Log;
import org.slf4j.Marker;
import org.slf4j.helpers.FormattingTuple;
import org.slf4j.helpers.MarkerIgnoringBase;
import org.slf4j.helpers.MessageFormatter;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import java.util.Properties;
import java.util.regex.Pattern;
/**
* <p>A simple implementation that delegates all log requests to the Google Android
* logging facilities. Note that this logger does not support {@link Marker}.
* Methods taking marker data as parameter simply invoke the eponymous method
* without the Marker argument, discarding any marker data in the process.</p>
* <p/>
* <p>The logging levels specified for SLF4J can be almost directly mapped to
* the levels that exist in the Google Android platform. The following table
* shows the mapping implemented by this logger.</p>
* <p/>
* <table border="1">
* <tr><th><b>SLF4J<b></th><th><b>Android</b></th></tr>
* <tr><td>TRACE</td><td>{@link Log#VERBOSE}</td></tr>
* <tr><td>DEBUG</td><td>{@link Log#DEBUG}</td></tr>
* <tr><td>INFO</td><td>{@link Log#INFO}</td></tr>
* <tr><td>WARN</td><td>{@link Log#WARN}</td></tr>
* <tr><td>ERROR</td><td>{@link Log#ERROR}</td></tr>
* </table>
* <p/>
* <p>Use loggers as usual:
* <ul>
* <li>
* Declare a logger<br/>
* <code>private static final Logger logger = LoggerFactory.getLogger(MyClass.class);</code>
* </li>
* <li>
* Invoke logging methods, e.g.,<br/>
* <code>logger.debug("Some log message. Details: {}", someObject);</code><br/>
* <code>logger.debug("Some log message with varargs. Details: {}, {}, {}", someObject1, someObject2, someObject3);</code>
* </li>
* </ul>
* </p>
* <p/>
* <p>Logger instances created using the LoggerFactory are named according to the fully qualified
* class name of the class given as a parameter.
*
* @author Andrey Korzhevskiy <a.korzhevskiy@gmail.com>
* @author Philip Schiffer <philip.schiffer@gmail.com
*/
public class AndroidLoggerAdapter extends MarkerIgnoringBase {
private static final long serialVersionUID = -1227274521521287937L;
private static final String NO_MESSAGE = "";
private static final StackTraceElement NOT_FOUND = new StackTraceElement(NO_MESSAGE, NO_MESSAGE, NO_MESSAGE, 0);
// Properties
private static final String CONFIGURATION_FILE = "logger.properties";
private static final Properties ANDROID_LOGGER_PROPERTIES = new Properties();
// Current log level and tag
private static LogLevel sLogLevel = LogLevel.INFO;
private static String sLogTag = "Slf4jAndroidLogger";
/**
* All system properties used by {@code AndroidLogger} start with this prefix
*/
public static final String SYSTEM_PREFIX = "de.psdev.slf4j.android.logger.";
public static final String DEFAULT_LOG_LEVEL_KEY = SYSTEM_PREFIX + "defaultLogLevel";
public static final String LOG_TAG_KEY = SYSTEM_PREFIX + "logTag";
/**
* Initialize properties read from properties file
*/
static {
InputStream propertiesInputStream = null;
final ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader();
if (threadClassLoader != null) {
propertiesInputStream = threadClassLoader.getResourceAsStream(CONFIGURATION_FILE);
} else {
propertiesInputStream = ClassLoader.getSystemResourceAsStream(CONFIGURATION_FILE);
}
if (propertiesInputStream != null) {
try {
ANDROID_LOGGER_PROPERTIES.load(propertiesInputStream);
propertiesInputStream.close();
} catch (IOException ignored) {
// ignored
}
}
// Init properties
final String defaultLogLevelString = getStringProperty(DEFAULT_LOG_LEVEL_KEY, null);
if (defaultLogLevelString != null) {
setLogLevel(stringToLevel(defaultLogLevelString));
}
setLogTag(getStringProperty(LOG_TAG_KEY, "Slf4jAndroidLogger"));
}
private final Pattern mClassNamePattern;
/**
* Package access allows only {@link AndroidLoggerFactory} to instantiate
* SimpleLogger instances.
*/
AndroidLoggerAdapter(final String tag) {
name = tag;
mClassNamePattern = Pattern.compile(name + "(\\$+.*)?");
}
public static LogLevel getLogLevel() {
return sLogLevel;
}
public static void setLogLevel(final LogLevel logLevel) {
sLogLevel = logLevel;
}
public static String getLogTag() {
return sLogTag;
}
public static void setLogTag(final String logTag) {
sLogTag = logTag;
}
/**
* Is this logger instance enabled for the VERBOSE level?
*
* @return True if this Logger is enabled for level VERBOSE, false otherwise.
*/
@Override
public boolean isTraceEnabled() {
return isLevelEnabled(LogLevel.TRACE);
}
/**
* Log a message object at level VERBOSE.
*
* @param msg - the message object to be logged
*/
@Override
public void trace(final String msg) {
log(LogLevel.TRACE, msg, null);
}
/**
* Log a message at level VERBOSE according to the specified format and
* argument.
* <p/>
* <p>
* This form avoids superfluous object creation when the logger is disabled
* for level VERBOSE.
* </p>
*
* @param format the format string
* @param arg the argument
*/
@Override
public void trace(final String format, final Object arg) {
formatAndLog(LogLevel.TRACE, format, arg);
}
/**
* Log a message at level VERBOSE according to the specified format and
* arguments.
* <p/>
* <p>
* This form avoids superfluous object creation when the logger is disabled
* for the VERBOSE level.
* </p>
*
* @param format the format string
* @param arg1 the first argument
* @param arg2 the second argument
*/
@Override
public void trace(final String format, final Object arg1, final Object arg2) {
formatAndLog(LogLevel.TRACE, format, arg1, arg2);
}
/**
* Log a message at level VERBOSE according to the specified format and
* arguments.
* <p/>
* <p>
* This form avoids superfluous object creation when the logger is disabled
* for the VERBOSE level.
* </p>
*
* @param format the format string
* @param argArray an array of arguments
*/
@Override
public void trace(final String format, final Object... argArray) {
formatAndLog(LogLevel.TRACE, format, argArray);
}
/**
* Log an exception (throwable) at level VERBOSE with an accompanying message.
*
* @param msg the message accompanying the exception
* @param t the exception (throwable) to log
*/
@Override
public void trace(final String msg, final Throwable t) {
log(LogLevel.TRACE, msg, t);
}
/**
* Is this logger instance enabled for the DEBUG level?
*
* @return True if this Logger is enabled for level DEBUG, false otherwise.
*/
@Override
public boolean isDebugEnabled() {
return isLevelEnabled(LogLevel.DEBUG);
}
/**
* Log a message object at level DEBUG.
*
* @param msg - the message object to be logged
*/
@Override
public void debug(final String msg) {
log(LogLevel.DEBUG, msg, null);
}
/**
* Log a message at level DEBUG according to the specified format and argument.
* <p>
* This form avoids superfluous object creation when the logger is disabled
* for level DEBUG.
* </p>
*
* @param format the format string
* @param arg the argument
*/
@Override
public void debug(final String format, final Object arg) {
formatAndLog(LogLevel.DEBUG, format, arg);
}
/**
* Log a message at level DEBUG according to the specified format and
* arguments.
* <p>
* This form avoids superfluous object creation when the logger is disabled
* for the DEBUG level.
* </p>
*
* @param format the format string
* @param arg1 the first argument
* @param arg2 the second argument
*/
@Override
public void debug(final String format, final Object arg1, final Object arg2) {
formatAndLog(LogLevel.DEBUG, format, arg1, arg2);
}
/**
* Log a message at level DEBUG according to the specified format and
* arguments.
* <p>
* This form avoids superfluous object creation when the logger is disabled
* for the DEBUG level.
* </p>
*
* @param format the format string
* @param argArray an array of arguments
*/
@Override
public void debug(final String format, final Object... argArray) {
formatAndLog(LogLevel.DEBUG, format, argArray);
}
/**
* Log an exception (throwable) at level DEBUG with an accompanying message.
*
* @param msg the message accompanying the exception
* @param t the exception (throwable) to log
*/
@Override
public void debug(final String msg, final Throwable t) {
log(LogLevel.DEBUG, msg, t);
}
/**
* Is this logger instance enabled for the INFO level?
*
* @return True if this Logger is enabled for the INFO level, false otherwise.
*/
@Override
public boolean isInfoEnabled() {
return isLevelEnabled(LogLevel.INFO);
}
/**
* Log a message object at the INFO level.
*
* @param msg - the message object to be logged
*/
@Override
public void info(final String msg) {
log(LogLevel.INFO, msg, null);
}
/**
* Log a message at level INFO according to the specified format and argument.
* <p>
* This form avoids superfluous object creation when the logger is disabled
* for the INFO level.
* </p>
*
* @param format the format string
* @param arg the argument
*/
@Override
public void info(final String format, final Object arg) {
formatAndLog(LogLevel.INFO, format, arg);
}
/**
* Log a message at the INFO level according to the specified format and
* arguments.
* <p>
* This form avoids superfluous object creation when the logger is disabled
* for the INFO level.
* </p>
*
* @param format the format string
* @param arg1 the first argument
* @param arg2 the second argument
*/
@Override
public void info(final String format, final Object arg1, final Object arg2) {
formatAndLog(LogLevel.INFO, format, arg1, arg2);
}
/**
* Log a message at level INFO according to the specified format and
* arguments.
* <p>
* This form avoids superfluous object creation when the logger is disabled
* for the INFO level.
* </p>
*
* @param format the format string
* @param argArray an array of arguments
*/
@Override
public void info(final String format, final Object... argArray) {
formatAndLog(LogLevel.INFO, format, argArray);
}
/**
* Log an exception (throwable) at the INFO level with an accompanying
* message.
*
* @param msg the message accompanying the exception
* @param t the exception (throwable) to log
*/
@Override
public void info(final String msg, final Throwable t) {
log(LogLevel.INFO, msg, t);
}
/**
* Is this logger instance enabled for the WARN level?
*
* @return True if this Logger is enabled for the WARN level, false
* otherwise.
*/
@Override
public boolean isWarnEnabled() {
return isLevelEnabled(LogLevel.WARN);
}
/**
* Log a message object at the WARN level.
*
* @param msg - the message object to be logged
*/
@Override
public void warn(final String msg) {
log(LogLevel.WARN, msg, null);
}
/**
* Log a message at the WARN level according to the specified format and
* argument.
* <p>
* This form avoids superfluous object creation when the logger is disabled
* for the WARN level.
* </p>
*
* @param format the format string
* @param arg the argument
*/
@Override
public void warn(final String format, final Object arg) {
formatAndLog(LogLevel.WARN, format, arg);
}
/**
* Log a message at the WARN level according to the specified format and
* arguments.
* <p>
* This form avoids superfluous object creation when the logger is disabled
* for the WARN level.
* </p>
*
* @param format the format string
* @param arg1 the first argument
* @param arg2 the second argument
*/
@Override
public void warn(final String format, final Object arg1, final Object arg2) {
formatAndLog(LogLevel.WARN, format, arg1, arg2);
}
/**
* Log a message at level WARN according to the specified format and
* arguments.
* <p>
* This form avoids superfluous object creation when the logger is disabled
* for the WARN level.
* </p>
*
* @param format the format string
* @param argArray an array of arguments
*/
@Override
public void warn(final String format, final Object... argArray) {
formatAndLog(LogLevel.WARN, format, argArray);
}
/**
* Log an exception (throwable) at the WARN level with an accompanying
* message.
*
* @param msg the message accompanying the exception
* @param t the exception (throwable) to log
*/
@Override
public void warn(final String msg, final Throwable t) {
log(LogLevel.WARN, msg, t);
}
/**
* Is this logger instance enabled for level ERROR?
*
* @return True if this Logger is enabled for level ERROR, false otherwise.
*/
@Override
public boolean isErrorEnabled() {
return isLevelEnabled(LogLevel.ERROR);
}
/**
* Log a message object at the ERROR level.
*
* @param msg - the message object to be logged
*/
@Override
public void error(final String msg) {
log(LogLevel.ERROR, msg, null);
}
/**
* Log a message at the ERROR level according to the specified format and
* argument.
* <p>
* This form avoids superfluous object creation when the logger is disabled
* for the ERROR level.
* </p>
*
* @param format the format string
* @param arg the argument
*/
@Override
public void error(final String format, final Object arg) {
formatAndLog(LogLevel.ERROR, format, arg);
}
/**
* Log a message at the ERROR level according to the specified format and
* arguments.
* <p>
* This form avoids superfluous object creation when the logger is disabled
* for the ERROR level.
* </p>
*
* @param format the format string
* @param arg1 the first argument
* @param arg2 the second argument
*/
@Override
public void error(final String format, final Object arg1, final Object arg2) {
formatAndLog(LogLevel.ERROR, format, arg1, arg2);
}
/**
* Log a message at level ERROR according to the specified format and
* arguments.
* <p>
* This form avoids superfluous object creation when the logger is disabled
* for the ERROR level.
* </p>
*
* @param format the format string
* @param argArray an array of arguments
*/
@Override
public void error(final String format, final Object... argArray) {
formatAndLog(LogLevel.ERROR, format, argArray);
}
/**
* Log an exception (throwable) at the ERROR level with an accompanying
* message.
*
* @param msg the message accompanying the exception
* @param t the exception (throwable) to log
*/
@Override
public void error(final String msg, final Throwable t) {
log(LogLevel.ERROR, msg, t);
}
private void formatAndLog(final LogLevel logLevel, final String format, final Object... argArray) {
if (isLevelEnabled(logLevel)) {
final FormattingTuple ft = MessageFormatter.arrayFormat(format, argArray);
log(logLevel, ft.getMessage(), ft.getThrowable());
}
}
private void log(final LogLevel logLevel, final String message, final Throwable throwable) {
if (isLevelEnabled(logLevel)) {
switch (logLevel.getAndroidLogLevel()) {
case Log.VERBOSE:
logAndroidVerbose(message, throwable);
break;
case Log.DEBUG:
logAndroidDebug(message, throwable);
break;
case Log.INFO:
logAndroidInfo(message, throwable);
break;
case Log.WARN:
logAndroidWarn(message, throwable);
break;
case Log.ERROR:
logAndroidError(message, throwable);
break;
default:
// nop
break;
}
}
}
private void logAndroidVerbose(final String message, final Throwable throwable) {
if (throwable != null) {
Log.v(getLogTag(), enhanced(message), throwable);
} else {
Log.v(getLogTag(), enhanced(message));
}
}
private void logAndroidDebug(final String message, final Throwable throwable) {
if (throwable != null) {
Log.d(getLogTag(), enhanced(message), throwable);
} else {
Log.d(getLogTag(), enhanced(message));
}
}
private void logAndroidInfo(final String message, final Throwable throwable) {
if (throwable != null) {
Log.i(getLogTag(), enhanced(message), throwable);
} else {
Log.i(getLogTag(), enhanced(message));
}
}
private void logAndroidWarn(final String message, final Throwable throwable) {
if (throwable != null) {
Log.w(getLogTag(), enhanced(message), throwable);
} else {
Log.w(getLogTag(), enhanced(message));
}
}
private void logAndroidError(final String message, final Throwable throwable) {
if (throwable != null) {
Log.e(getLogTag(), enhanced(message), throwable);
} else {
Log.e(getLogTag(), enhanced(message));
}
}
// Property getter
private static String getStringProperty(final String propertyName) {
String propertyValue = null;
try {
propertyValue = System.getProperty(propertyName);
} catch (SecurityException ignored) {
}
return propertyValue == null ? ANDROID_LOGGER_PROPERTIES.getProperty(propertyName) : propertyValue;
}
private static String getStringProperty(final String propertyName, final String defaultValue) {
final String prop = getStringProperty(propertyName);
return prop == null ? defaultValue : prop;
}
private static boolean getBooleanProperty(final String propertyName, final boolean defaultValue) {
final String prop = getStringProperty(propertyName);
return prop == null ? defaultValue : "true".equalsIgnoreCase(prop);
}
private String enhanced(final String message) {
final StackTraceElement caller = determineCaller();
final String classNameOnly = getClassNameOnly(caller.getClassName());
final String methodName = caller.getMethodName();
final int lineNumber = caller.getLineNumber();
final Thread thread = Thread.currentThread();
return String.format(Locale.ENGLISH, "%s [%s:%s:%s] %s", message, classNameOnly, methodName, lineNumber, thread);
}
private StackTraceElement determineCaller() {
final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
for (final StackTraceElement element : stackTrace) {
if (mClassNamePattern.matcher(element.getClassName()).matches()) {
return element;
}
}
return NOT_FOUND;
}
private static String getClassNameOnly(final String classNameWithPackage) {
final int lastDotPos = classNameWithPackage.lastIndexOf('.');
if (lastDotPos == -1) {
return classNameWithPackage;
}
return classNameWithPackage.substring(lastDotPos + 1);
}
/**
* Is the given log level currently enabled?
*
* @param logLevel is this level enabled?
*/
protected static boolean isLevelEnabled(final LogLevel logLevel) {
// log level are numerically ordered so can use simple numeric comparison
return logLevel.getAndroidLogLevel() >= sLogLevel.getAndroidLogLevel();
}
private static LogLevel stringToLevel(final String levelStr) {
if ("trace".equalsIgnoreCase(levelStr)) {
return LogLevel.TRACE;
}
if ("verbose".equalsIgnoreCase(levelStr)) {
return LogLevel.TRACE;
}
if ("debug".equalsIgnoreCase(levelStr)) {
return LogLevel.DEBUG;
}
if ("info".equalsIgnoreCase(levelStr)) {
return LogLevel.INFO;
}
if ("warn".equalsIgnoreCase(levelStr)) {
return LogLevel.WARN;
}
if ("error".equalsIgnoreCase(levelStr)) {
return LogLevel.ERROR;
}
// assume INFO by default
return LogLevel.INFO;
}
}